package org.bdware.doip.core.codec;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.nimbusds.jose.jwk.JWK;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.log4j.Logger;
import org.bdware.doip.core.crypto.CryptoUtils;
import org.bdware.doip.core.crypto.GlobalCertifications;
import org.bdware.doip.core.exception.DoDecodeException;
import org.bdware.doip.core.model.digitalObject.DigitalObject;
import org.bdware.doip.core.model.digitalObject.Element;
import org.bdware.doip.core.utils.DoipGson;
import org.bdware.irp.crypto.CertUtils;

import java.io.*;
import java.util.*;

public class DigitalObjectCodecImpl implements DigitalObjectCodec {

    Logger logger = Logger.getLogger(DigitalObjectCodecImpl.class);

    @Override
    public byte[] DoToByteArray(DigitalObject digitalObject) {
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(bo);

        Gson gson = DoipGson.getDoipGson();
        String jsonSegments = gson.toJson(digitalObject);

        try {
            // add length information
            out.writeInt(jsonSegments.getBytes().length);
            out.write(jsonSegments.getBytes());
            if (digitalObject.elements != null) {
//                digitalObject.elements = sortElement(digitalObject.elements);
                for (Element e : digitalObject.elements) {
                    if (e.getData() != null) {
                        assert e.length == e.getData().length;
                        out.write(e.getData());
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bo.toByteArray();
    }

    public List<Element> sortElement(List<Element> eles){
        for(int i=0;i<eles.size();i++)
            for(int j=i+1;j<eles.size();j++){
                if(eles.get(i).id.compareTo(eles.get(j).id)>0){
                    Element temp = eles.get(i);
                    eles.set(i, eles.get(j));
                    eles.set(j,temp);
                }
            }
        return eles;
    }

    @Override
    public DigitalObject ByteArrayToDo(byte[] doBytes) throws DoDecodeException, IOException {
        if (doBytes == null) return null;
        DigitalObject d;
        DataInputStream input = new DataInputStream(new ByteArrayInputStream(doBytes));

        // Json segment length
        int metaLen = input.readInt();
        if (metaLen > doBytes.length) {
            logger.info("invalid DO byte array");
            throw new DoDecodeException("invalid DO byte array");
        }
        byte[] content = new byte[metaLen];
        int eof = input.read(content);
//        logger.debug("digital Object: " + new String(content));
        Gson gson = DoipGson.getDoipGson();
        d = gson.fromJson(new String(content), DigitalObject.class);

        if (d.elements == null || eof == -1 || input.available() == 0) return d;

//        d.elements.sort(Comparator.comparing(o -> o.id));
        for (int i = 0; i < d.elements.size(); i++) {
            if (input.available() < d.elements.get(i).length) {
                throw new DoDecodeException("Unexpected element data length");
            }
            byte[] data = new byte[d.elements.get(i).length];
            input.read(data);
            d.elements.get(i).setData(data);
        }
        return d;
    }

    @Override
    public ArrayList<byte[]> DoToSegments(DigitalObject digitalObject) {
        ArrayList<byte[]> segStrings = new ArrayList<>();
        segStrings.add(DoipGson.getDoipGson().toJson(digitalObject).getBytes());
        if (digitalObject.elements != null) {
            for (Element e : digitalObject.elements) {
                if (e.getData() == null) continue;
                JsonObject eid = new JsonObject();
                eid.addProperty("id", e.id);
                segStrings.add(DoipGson.getDoipGson().toJson(eid).getBytes());
                segStrings.add(e.getData() == null ? "".getBytes() :
                        ("@\n" + e.getData().length + "\n" + new String(e.getData())).getBytes());
            }
        }

        //增加签名字段
        if (digitalObject.isSigned()) {
            ByteBuf signPayload = Unpooled.directBuffer();
            for (byte[] seg : segStrings) {
                signPayload.writeBytes(seg);
            }
            byte[] payloadBytes = new byte[signPayload.readableBytes()];
            signPayload.readBytes(payloadBytes);
            try {
                segStrings.add(Objects.requireNonNull(CertUtils.Sign(payloadBytes, GlobalCertifications.getGlobalJWK())).getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return segStrings;
    }

    @Override
    public DigitalObject SegmentsToDo(ArrayList<byte[]> segStrings) throws DoDecodeException {

        if (segStrings == null || segStrings.size() < 1) {
            logger.error("input segment string is empty");
            return null;
        }
        Iterator<byte[]> segIt = segStrings.iterator();
        String jsonSegment = new String(segIt.next());
        logger.debug("DO json segments: " + jsonSegment);
        DigitalObject digitalObject;
        try {
            digitalObject = DoipGson.getDoipGson().fromJson(jsonSegment, DigitalObject.class);
            if (digitalObject.id == null) throw new DoDecodeException("not digital object segments");
        } catch (Exception e) {
            throw new DoDecodeException("not digital object segments");
        }
        HashMap<String, byte[]> elementDataMap = new HashMap<>();

        int index = 0;
        while (segIt.hasNext() && digitalObject.elements != null && index < digitalObject.elements.size()) {
            String elementIDSeg = new String(segIt.next());
            JsonObject eid = new Gson().fromJson(elementIDSeg, JsonObject.class);
            String elementID = eid.get("id").getAsString();
            if (elementID == null) {
                logger.error("element id segments error: " + elementIDSeg);
                break;
            }
            if (!segIt.hasNext()) {
                logger.error("element data segments not found!");
                break;
            }
            byte[] elementDataSeg = segIt.next();
            BufferedReader br = new BufferedReader(new StringReader(new String(elementDataSeg)));
            String elementData = "";
            while (true) {
                try {
                    String line = br.readLine();
                    if (line == null || line.length() == 0) break;
                    if (line.charAt(0) == '@') {
                        int chunkLength = Integer.parseInt(br.readLine());
                        char[] chunk = new char[chunkLength];
                        br.read(chunk, 0, chunkLength);
                        elementData += new String(chunk);
                    } else {
                        if (elementData.length() > 0) {
                            logger.error("invalid chunks");
                        }
                        break;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (elementData.length() > 0) {
                elementDataMap.put(elementID, elementData.getBytes());
            } else {
                elementDataMap.put(elementID, elementDataSeg);
            }
        }

        if (digitalObject.isSigned() && segIt.hasNext()) {
            String signatureSeg = new String(segIt.next());

            ByteBuf signPayload = Unpooled.directBuffer();
            for (int i = 0; i < segStrings.size() - 1; i++) {
                signPayload.writeBytes(segStrings.get(i));
            }
            byte[] payloadBytes = new byte[signPayload.readableBytes()];
            signPayload.readBytes(payloadBytes);

            try {
                if (!CertUtils.verify(
                        payloadBytes,
                        signatureSeg,
                        JWK.parse(digitalObject.attributes.get("publicKey").getAsString())))
                    logger.warn("Verify signature error!");
            } catch (Exception e) {
                logger.warn("Verify signature error!");
                e.printStackTrace();
                return digitalObject;
            }
        }

        if (segIt.hasNext()) logger.warn("unexcepted segment");

        if (digitalObject.elements != null)
            for (Element e : digitalObject.elements) {
                e.setData(elementDataMap.get(e.id));
            }
        return digitalObject;
    }

}
